# 前面的文件实现了下载搜索结果的第一页
# 这个相当于在 api 下下载多页的音乐
# https://music.91q.com/v1/search?sign=44485bdbc79ce3f9fb0c4134ac2be401
# &word=%E7%8E%8B%E5%8A%9B%E5%AE%8F
# &type=1&pageSize=20&appid=16073360&timestamp=1742303717

import requests
from bs4 import BeautifulSoup
import logging
import pandas as pd
import hashlib
import time


class MusicScraper:
    def __init__(self, word, appid, baseurl, secret):
        self.word = word
        self.appid = appid
        self.baseurl = baseurl
        self.headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
        }
        self.secret = secret

    def gen_timestamp(self):
        """
        生成当前时间戳
        :return: 整数型时间戳（10位整数）
        """
        return int(time.time())

    def get_total_page(self):
        url = f'{self.baseurl}/search'
        params = {
            "word": self.word
        }
        try:
            response = requests.get(url, headers=self.headers, params=params)
        except requests.exceptions.RequestException as e:
            logging.error(f"Error to visit the site: {e}")
            return None
        soup = BeautifulSoup(response.text, 'html.parser')
        p = soup.find_all(name='ul', class_='el-pager')
        '''此时的p举例
        <ul class="el-pager">
            <li class="number active">1</li>
            <li class="number">2</li>
            <li class="number">3</li>
        </ul>
        '''
        if p:
            t_p = p[0].find_all('li', class_='number')[-1].text
            total_page = int(t_p)
        else:
            total_page = 1
        return total_page

    def create_songinfo_sign(self, cur_page=1):
        """
        首先创建一个sign用于从/v1/search接口获取歌曲信息
        :param timestamp: 时间戳
        :return: 返回签名字符串
        """
        total_page = self.get_total_page()
        timestamp = self.gen_timestamp()
        if total_page > 1:
            # 多页信息的歌手sign
            r = f'appid={self.appid}&pageNo={cur_page}&pageSize=20&timestamp={timestamp}&type=1&word={self.word}{self.secret}'
        else:
            # 单页信息歌手创建sign
            r = f'appid={self.appid}&pageSize=20&timestamp={timestamp}&type=1&word={self.word}{self.secret}'
        return hashlib.md5(r.encode('utf-8')).hexdigest()
        # 首先， r.encode('utf-8') 将字符串 r 转换为字节码
        # 然后， hashlib.md5() 对这些字节码进行 MD5 加密，生成一个哈希对象
        # 最后， hexdigest() 将这个哈希对象转换为十六进制字符串 (源代码的使用需求)

    def build_fetching_params(self, cur_page=1):
        timestamp = self.gen_timestamp()
        sign = str(self.create_songinfo_sign(cur_page))
        # print(sign)
        return {
            "word": self.word,
            "type": 1,
            "pageSize": 20,
            "appid": self.appid,
            "timestamp": timestamp,
            "sign": sign
        }

    def fetch_music_info(self, cur_page=1):
        url = f'{self.baseurl}/v1/search'
        params = self.build_fetching_params(cur_page)
        try:
            response = requests.get(url, headers=self.headers, params=params)
            if response.history != []:   # history 会记录重定向过程的信息
                logging.info(f'Sites redirected: {response.history}')
                if response.status_code != 200:
                    logging.error(
                        f'Requests Error: {response.status_code}, {response.reason}')
                response.raise_for_status()
        except requests.exceptions.RequestException as e:
            logging.error(f"Error in fetching music info: {e}")
            return None

        soup = BeautifulSoup(response.text, 'html.parser')
        # print(soup.prettify)
        box = soup.find_all(name='li', class_='pr t clearfix')
        song_box = soup.find_all(name='div', class_='song-box')
        singer_box = soup.find_all(name='div', class_='artist ellipsis')
        album_box = soup.find_all(name='div', class_='album ellipsis')

        music_info = []
        for song, singer_list, album in zip(song_box, singer_box, album_box):
            # 查找歌曲
            musicname = song.find('a').text
            song_id = song.find('a').get('href')
            # musicid = "https://music.91q.com" + song_id
            T_idx = song_id.find('T')
            SongID = song_id[T_idx:]
            # 查找歌手
            i = singer_list.find_all(name='a')
            singer = ", ".join(a.get_text(strip=True) for a in i)
            #·查找专辑
            album = album.find('a').text
            album_id = album.find('a').get('href')
            P_idx = album_id.find('P')
            AlbumID = album_id[P_idx:]

            music_info.append(
                {'musicname': musicname, 'singer': singer, 'SongID': SongID, 'album': album, "AlbumID": AlbumID})
            print(music_info)
        return pd.DataFrame(music_info)
    
    def fetch_all_music_info(self):
        total_page = self.get_total_page()
        all_music_info = []
        for cur_page in range(1, total_page + 1):
            music_info = self.fetch_music_info(cur_page=cur_page)
            all_music_info.append(music_info)
        if all_music_info:
            return pd.concat(all_music_info, ignore_index=True)

    def create_link_sign(self, SongID):
        timestamp = self.gen_timestamp()
        appid = self.appid
        secret = self.secret
        TSID = SongID
        r = f'TSID={TSID}&appid={appid}&timestamp={timestamp}{secret}'
        return hashlib.md5(r.encode('utf-8')).hexdigest()

    def build_tracklink_params(self, SongID):
        timestamp = self.gen_timestamp()
        sign = self.create_link_sign(SongID)
        return {
            "appid": self.appid,
            "timestamp": timestamp,
            "sign": sign,
            "TSID": SongID
        }     

    def get_mp3_url(self, SongID):
        """
        发送 HTTP GET 请求并处理常见异常
        :param url: 请求地址
        :param params: 请求参数
        :return: 响应对象或 None（如果请求失败）
        其中会有path 返回可以下载的MP3地址
        """
        url = f'{self.baseurl}/v1/song/tracklink'
        params = self.build_tracklink_params(SongID)
        try:
            response = requests.get(url, params=params, timeout=10)  # 设置超时时间
            response.raise_for_status()  # 检查 HTTP 状态码
            data = response.json()
            mp3_url = data.get('data', {}).get('path')
            # 字典的 get 方法，用于安全地访问嵌套字典中的值。取得键的相应值，如果没有就返回空的{}
            if not mp3_url or not isinstance(mp3_url, str):
                logging.warning(f"No MP3 URL found for TSID: {SongID}")
            return None
        except requests.exceptions.RequestException as e:
            logging.error(f"Request exception occurred error for TSID {SongID}: {e}")
        return mp3_url

    def write_to_file(self, filename, SongID):
        """
        将 MP3 URL 写入文件
        :param mp3_url: MP3 URL
        :param filename: 文件名
        """
        mp3_url = self.get_mp3_url(SongID)
        filename = re.sub(r'[\\/:*?"<>|]', '_', filename)
        if not mp3_url:
            logging.warning("MP3 URL is empty. Nothing to write.")
            return
        
        try:
            response = requests.get(mp3_url, headers=self.headers)
            response.raise_for_status()
            with open(filename, 'wb') as file:
                file.write(response.content)
            logging.info(f"MP3 data written to {filename}")
        except Exception as e:
            logging.error(f"Failed to download to file: {e}")


if __name__ == "__main__":
    logging.basicConfig(
    level=logging.INFO,  # 设置日志级别为 INFO
    format='%(asctime)s - %(levelname)s - %(message)s',  # 设置日志格式
    handlers=[logging.StreamHandler()]  # 输出到终端
)   

    word = input("请输入歌手名：")
    secret = "0b50b02fd0d73a9c4c8c3a781c30845f"
    appid = "16073360"
    baseurl = "https://music.91q.com"
# 调用，实例一个对象
    try:
        scraper = MusicScraper(word, appid, baseurl, secret)

        all_music_info = scraper.fetch_all_music_info()
        if all_music_info is not None:
             for index, row in all_music_info.iterrows():
                 SongID = row['SongID']
                 filename = f"{row['musicname']}_{SongID}.mp3"
                 scraper.write_to_file(filename, SongID)
    except  Exception as e:
        logging.error(f"Error occurred: {e}")
        